package org.ovirt.engine.core.bll.scheduling.utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.ovirt.engine.core.bll.scheduling.SlaValidator;
import org.ovirt.engine.core.common.businessentities.Cluster;
import org.ovirt.engine.core.common.businessentities.MigrationSupport;
import org.ovirt.engine.core.common.businessentities.VDS;
import org.ovirt.engine.core.common.businessentities.VM;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dao.VmDao;
import org.ovirt.engine.core.dao.VmStatisticsDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Find a VM that can be migrated and a set of hosts that can potentially
 * receive it while not violating the utilization rules.
 */
public class FindVmAndDestinations {
    private static final Logger log = LoggerFactory.getLogger(FindVmAndDestinations.class);

    private Cluster cluster;
    private int highCpuUtilization;
    private long requiredMemory;

    public static class Result {
        private List<VDS> destinationHosts;
        private VM vmToMigrate;

        public Result(VM vmToMigrate, List<VDS> destinationHosts) {
            this.vmToMigrate = vmToMigrate;
            this.destinationHosts = destinationHosts;
        }

        public List<VDS> getDestinationHosts() {
            return destinationHosts;
        }

        public VM getVmToMigrate() {
            return vmToMigrate;
        }
    }

    public FindVmAndDestinations(Cluster cluster, int highCpuUtilization, long requiredMemory) {
        this.cluster = cluster;
        this.highCpuUtilization = highCpuUtilization;
        this.requiredMemory = requiredMemory;
    }

    public Optional<Result> invoke(List<VDS> sourceHosts,
            List<VDS> destinationHosts,
            VmDao vmDao,
            VmStatisticsDao vmStatisticsDao) {

        // Iterate over source hosts until you find valid vm to migrate, hosts sorted by cpu usage
        for (VDS sourceHost : sourceHosts){
            // Get list of all migratable vms on host
            List<VM> migratableVmsOnHost = getMigratableVmsRunningOnVds(vmDao, sourceHost.getId());
            if (migratableVmsOnHost.isEmpty()) {
                continue;
            }

            // Statistics are needed for sorting by cpu usage
            for (VM vm : migratableVmsOnHost) {
                vm.setStatisticsData(vmStatisticsDao.get(vm.getId()));
            }

            // Sort vms by cpu usage
            Collections.sort(migratableVmsOnHost, VmCpuUsageComparator.INSTANCE);
            for (VM vmToMigrate : migratableVmsOnHost){
                // Check if vm not over utilize memory or CPU of destination hosts
                List<VDS> validDestinationHosts = getValidHosts(
                        destinationHosts, cluster, vmToMigrate, highCpuUtilization, requiredMemory);

                if (!validDestinationHosts.isEmpty()){
                    log.debug("Vm '{}' selected for migration", vmToMigrate.getName());
                    return Optional.of(new Result(vmToMigrate, validDestinationHosts));
                }
            }
        }
        return Optional.empty();
    }

    /**
     * The predicted CPU the CPU that the VM will take considering
     * how many cores it has and how many cores the host has.
     * @return
     *          predicted vm cpu
     */
    protected int getPredictedVmCpu(VM vm, VDS vds, boolean countThreadsAsCores) {
        Integer effectiveCpuCores = SlaValidator.getEffectiveCpuCores(vds, countThreadsAsCores);
        if (vm.getUsageCpuPercent() != null && effectiveCpuCores != null) {
            return (vm.getUsageCpuPercent() * vm.getNumOfCpus())
                    / effectiveCpuCores;
        }
        return 0;
    }

    /**
     * Return all VMs that run on a host and can be migrated away.
     *
     * This method is to be considered private. It is protected to be available
     * from unit tests.
     *
     * @param vmDao The data source to get the VM information
     * @param hostId Id of a host the returned VMs run at
     * @return list od VM that run on host and can be migrated
     */
    protected List<VM> getMigratableVmsRunningOnVds(final VmDao vmDao, final Guid hostId) {
        return vmDao.getAllRunningForVds(hostId).stream()
                .filter(vm -> vm.getMigrationSupport() == MigrationSupport.MIGRATABLE)
                .collect(Collectors.toList());
    }

    /**
     * Pre-filter candidate hosts with regards to the balancing policy. The default
     * implementation does not allow putting VM on a host that would become
     * over-utilized to prevent bouncing.
     *
     * @param candidates candidate hosts
     * @param cluster cluster reference
     * @param vm vm to be migrated
     * @param highCpuUtilization CPU over-utilization threshold in percents
     * @param minimalFreeMemory Memory over-utilization threshold in MB
     * @return list of hosts that satisfy the balancing contraints
     */
    private List<VDS> getValidHosts(Collection<VDS> candidates, Cluster cluster, VM vm, int highCpuUtilization, long minimalFreeMemory) {
        List<VDS> result = new ArrayList<>();

        for (VDS vds: candidates) {
            int predictedVmCpu = getPredictedVmCpu(vm, vds, cluster.getCountThreadsAsCores());
            if (vds.getUsageCpuPercent() + predictedVmCpu <= highCpuUtilization
                    && vds.getMaxSchedulingMemory() - vm.getMemSizeMb() > minimalFreeMemory) {
                result.add(vds);
            }
        }

        return result;
    }
}
